package Command;
use strict;
use warnings;
use utf8;

require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(ProcessCommand);

use EV;
require Chanel;
require Login;
use Util;
use Log::Syslog::Fast ':all';

our %waiting;

our %command = (
	unreg => \&cmd_unreg,
	reg => \&cmd_reg,
	chlog => \&cmd_chlog,
	setpass => \&cmd_setpass,
	login => \&cmd_login,
	check => \&cmd_check,
	message => \&cmd_message,
	private => \&cmd_private,
	join => \&cmd_join_chanel,
	leave => \&cmd_leave_chanel,
	create => \&cmd_create_chanel,
	remove => \&cmd_remove_chanel,
	list => \&cmd_list_chanel,
	ping => \&cmd_ping,
	command => \&cmd_command,
	cast => \&cmd_cast,
);
our %BroadCast = ('1'=>{type=>"message",from=>"server",text=>"server started ".ts(),ts=>ts(),ats=>time()});
our $iBroadCastHas = 1;
our $iBroadCast = 1;
our %Topic = ('1'=>{type=>"notice",param2=>"server started ".fts(),param1=>"topic",ats=>time()});
our $iTopic = 1;

our $logger;
sub logsend{
	unless($logger){
		$logger = Log::Syslog::Fast->new(LOG_UNIX, "/dev/log", 0, LOG_LOCAL0, LOG_INFO, $main::params{log}, "srv");
	}
	$logger->send($_[0]);
}


# принимает сообщение, обрабатывает и отдает результат
# что далать с результатом решает тот кто запросил обработку
sub ProcessCommand {
	my ($msg,$fh,$sender) = @_;
	my $ret;
	if(my $handler = $command{$msg->{cmd} || ""}){
		if($ret = $handler->($msg,$fh,$sender)){
			unless($ret->{nowait}){
				$ret->{rid} ||= $msg->{rid};
				$ret->{command} ||= $msg->{cmd};
			}else{
				return undef;
			}
		}
	}else{
		$ret = {
			command => $msg->{cmd},
			error => 405,
			text => "invalid command",
		};
	}
	return $ret;
}

# обработчики команд
sub cmd_reg {
	my ($msg,$fh,$sender) = @_;
	if(my $login = Login::check_auth($msg)){
		if(Login::can_reg($login)){
			if(my $err = Login::register($msg->{userlogin},$msg->{userpass},$msg->{tmp})){
				return $err;
			}else{
				return {text=>"ok [$msg->{userlogin}] $msg->{userpass}"};
			}
		}else{
			return {error=>403,text=>"not authority",};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_unreg {
	my ($msg,$fh,$sender) = @_;
	if(my $login = Login::check_auth($msg)){
		if(Login::can_reg($login)){
			if(my $err = Login::unregister($msg->{user})){
				return $err;
			}else{
				return {text=>"ok [$msg->{user}]"};
			}
		}else{
			return {error=>403,text=>"not authority",};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_chlog {
	my ($msg) = @_;
	my $login;
	if($login = Login::check_auth($msg)){
		if(my $err = Login::change_login($login,$msg->{newlogin})){
			return $err;
		}else{
			return {type=>"notice",param1=>"newlogin",param2=>$msg->{newlogin}};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_setpass {
	my ($msg) = @_;
	my $login;
	if($login = Login::check_auth($msg)){
		if(my $err = Login::change_pass($login,$msg->{newpass})){
			return $err;
		}else{
			return {type=>"notice",param1=>"newpass",param2=>$msg->{newpass}};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_login {
	my ($msg,$fh,$sender) = @_;
	return Login::login($msg->{login},$msg->{pass},$msg->{ip},$fh,$sender);
}

sub cmd_ping {
	my ($msg) = @_;
	Login::check_auth($msg);
	return 0;
}
sub cmd_command {
	my ($msg) = @_;
	my $login = Login::check_auth($msg);
	unless($login eq "admin"){
	}
	elsif($msg->{param1} eq "sstop"){
		EV::break EV::BREAK_ALL;
		return {text=>"ok"};
	}
	elsif($msg->{param1} eq "dstart"){
		if($main::params{prof}){
			DB::enable_profile("prof.$$.out");
		}
		return {text=>"ok"};
	}
	elsif($msg->{param1} eq "dstop"){
		if($main::params{prof}){
			DB::disable_profile();
			DB::finish_profile();
		}
		return {text=>"ok"};
	}
	elsif($msg->{param1} eq "stat"){
		if($msg->{param2} eq  "waiting"){
			my @k = keys %waiting;
			return {type=>"message",text=>"waiting: ".scalar(@k),};
		}
		elsif($msg->{param2} eq  "sids"){
			my @k = keys %Login::sids;
			return {type=>"message",text=>"sids: ".scalar(@k),};
		}
		elsif($msg->{param2} eq  "chanels"){
			my @k = keys %Chanel::chanels;
			return {type=>"message",text=>"chanels: ".scalar(@k),};
		}
		return {text=>"ok"};
	}
	elsif($msg->{param1} eq "topic"){
		delete $Topic{$iTopic};
		$Topic{++$iTopic} = {type=>"notice",param2=>$msg->{param2},param1=>"topic",ats=>time()};
		return {text=>"ok"};
	}
	elsif($msg->{param1} eq "info"){
		if(my $rec = Login::get_login($msg->{param2})){
			my $str = "[$msg->{param2}] reg=".fts($rec->{r});
			if($rec->{i}){
				$str .= " in=".fts($rec->{i});
			}
			if($rec->{o}){
				$str .= " out=".fts($rec->{o});
				if($rec->{i}){
					$str .= " ses=".sec2str($rec->{o} - $rec->{i});
				}
			}
			return {type=>"message", text => $str};
		}else{
			return {type=>"message", text=>"[$msg->{param2}] not found"};
		}
	}
	return 0;
}
sub cmd_cast {
	my ($msg) = @_;
	my $login = Login::check_auth($msg);
	if($login eq "admin"){
		if($iBroadCast - $iBroadCastHas > 100){
			delete @BroadCast{($iBroadCastHas .. ($iBroadCastHas+50))};
			$iBroadCastHas += 50;
		}
		my $ret = {
			ats=>time()
		};
		if($msg->{param1} eq "message"){
			$ret->{type} = "message";
			$ret->{from} = $msg->{param2};
			$ret->{text} = $msg->{param3};
			$ret->{ts} = ts();
		}
		elsif($msg->{param1} eq "exec"){
			$ret->{type} = "notice";
			$ret->{param1} = "exec";
			$ret->{param2} = $msg->{param2};
			if($msg->{param3}){
				delete $Topic{$iTopic};
				$Topic{++$iTopic} = {type=>"notice",param2=>$msg->{param3},param1=>"topic",ats=>time()};
			}
		}
		$BroadCast{++$iBroadCast} = $ret;
		return {text=>"ok"};
	}
	return 0;
}
sub cmd_message {
	my ($msg) = @_;
	my $login;
	if($login = Login::check_auth($msg)){
		if(Chanel::can_message($msg->{chanel},$login)){
			if(my $diff = Login::check_wipe($login)){
				return {error=>403,text=>"try over $diff sec",};
			}else{
				my $text = html($msg->{text});
				logsend("[".$login."] ".$text);
				put_message({type=>"message",rid=>$msg->{rid},from=>$login,text=>$text,chanel=>$msg->{chanel}});
				return {text=>"ok"};
			}
		}else{
			return {error=>403,text=>"permission",};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_raw {
	my ($msg) = @_;
	my $login;
	if($login = Login::check_auth($msg)){
		if(Chanel::can_raw($msg->{chanel},$login)){
			my %tmp = %$msg;
			delete @tmp{('sid','command')};
			put_message(\%tmp);
			return {text=>"ok"};
		}else{
			return {error=>403,text=>"not authority",};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
sub cmd_private {
	my ($msg) = @_;
	my $login;
	if($login = Login::check_auth($msg)){
		if(my $diff = Login::check_wipe($login)){
			return {error=>403,text=>"try over $diff sec",};
		}else{
			my $text = html($msg->{text});
			logsend("[".$login."] ".$text);
			put_private({type=>"message",rid=>$msg->{rid}, from=>$login, text=>$text,to => $msg->{to}, private => 1});
			return {text=>"ok",};
		}
	}else{
		return {error=>401,text=>"not authority",};
	}
}
# вот здесь нужно ждать
sub cmd_check {
	my ($msg,$fh,$sender) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}else{
		# получаем список сообщений
		my $list = build_list($login);
		#  если есть ли новые сообщения
		if(@$list){
			return {type=>"list",list=>$list,};
		}else{
			# ничего не возвращаем, потому что нету
			# както надо указать, что здесь мы ничего не возвращаем
			# не потому что просто игнорим,
			# а потому что нужно подождать ответа
			$waiting{$login} = [$fh,$sender,$msg->{rid}];
			return undef;
		}
	}
}
sub cmd_list_chanel {
	my ($msg) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}elsif(Chanel::can_list($msg->{chanel},$login)){
		if(my $list = Chanel::list_chanel($login,$msg->{chanel})){
			return {type=>"notice",chanel=>$msg->{chanel},param1=>"list",param2=>$list};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel",};
		}
	}else{
		return {error=>412,text=>"need chanel",};
	}
}
sub cmd_join_chanel {
	my ($msg) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}elsif(Chanel::can_join($msg->{chanel},$login)){
		if(my $title = Chanel::join_chanel($login,$msg->{chanel})){
			return {chanel=>$msg->{chanel},text=>"ok",title=>$title};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel"};
		}
	}else{
		return {error=>412,text=>"need chanel",};
	}
}
sub cmd_leave_chanel {
	my ($msg) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}elsif(Chanel::can_leave($msg->{chanel},$login)){
		if(Chanel::leave_chanel($login,$msg->{chanel})){
			#put_message({rid=>$msg->{rid},text => "leave [$login]",chanel=>$msg->{chanel}});
			return {chanel=>$msg->{chanel},text=>"ok",};
		}else{
			return {chanel=>$msg->{chanel},error=>404,text=>"bad chanel",};
		}
	}else{
		return {chanel=>$msg->{chanel},error=>412,text=>"need chanel",};
	}
}
sub cmd_create_chanel {
	my ($msg) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}elsif(Chanel::can_create($msg->{chanel},$login)){
		if(Chanel::add_chanel($login,$msg->{chanel})){
			return {text=>"ok",};
		}else{
			return {error=>404,text=>"bad chanel",};
		}
	}else{
		return {error=>412,text=>"bad chanel",};
	}
}
sub cmd_remove_chanel {
	my ($msg) = @_;
	my $login;
	unless($login = Login::check_auth($msg)){
		return {error=>401,text=>"not authority",};
	}elsif(Chanel::can_remove($msg->{chanel},$login)){
		unless(my $e = Chanel::del_chanel($login,$msg->{chanel})){
			return {text=>"ok",};
		}else{
			return {error=>$e,text=>"bad chanel",};
		}
	}else{
		return {error=>412,text=>"need chanel",};
	}
}

sub put_private {
	my ($msg) = @_;
	$msg->{ts} = ts();
	$msg->{ats} = ats();
	for my $to(@{$msg->{to}}){
		# сохраняем сообщение
		if(my $l = $Login::logins{$to}){
			push @{$l->{private}},$msg;
			# если есть в списке ожидающих, то отправляем сразу
			if(my $target = delete $waiting{$to}){
				my $list;
				if(@{$list = build_list($to)}){
					# $target->[1] = sender(fh,msg)
					# $target->[0] = fh
					# $target->[2] = rid for last "check"
					$target->[1]->($target->[0],{type=>"list",list=>$list,rid=>$target->[2]});
				}
			}
		}
	}
}
# рассылка входящего сообщения
sub put_message {
	my ($msg) = @_;
	my $chanel = Chanel::get_chanel($msg->{chanel});
	unless($chanel){
		return;
	}
	$msg->{ts} = ts();
	$msg->{ats} = ats();
	# так как отдаем не больше 100 последних, то хранить больше не имеет смысл
	push @{$chanel->{messages}},$msg;
	if(@{$chanel->{messages}} > 500){
		splice @{$chanel->{messages}},0,350;
		$chanel->{shift} += 350;
	}
	$chanel->{last} = time();
	# пройти по всем ожидающим и отправить
	my @wdone;
	for my $login(keys %{$chanel->{list}}){
		if(my $target = $waiting{$login}){
			my $list;
			if(@{$list = build_list($login)}){
				$target->[1]->($target->[0],{type=>"list",list=>$list,rid=>$target->[2]});
				push @wdone,$login;
			}
		}
	}
	# удаляем всех кому отправили
	delete @waiting{@wdone};
}
sub build_list {
	my ($login) = @_;
	unless($login){
		# если сессии уже нет, то ничего не делаем
		return [];
	}
	my @list;

	my $lgn = $Login::logins{$login};
	my $lt;
	unless(($lt = $lgn->{topic}) && ($lt >= $iTopic)){
		$lgn->{topic} = $iTopic;
		push @list, $Topic{$iTopic};
	}
	unless(($lt = $lgn->{bc}||0) && ($lt >= $iBroadCast)){
		$lt++;
		push @list, @BroadCast{($lt .. $iBroadCast)};
		$lgn->{bc} = $iBroadCast;
	}
	if(@{$lgn->{private}}){
		push @list,@{$lgn->{private}};
		$lgn->{private} = [];
	}
	for my $id(keys %{$lgn->{chanels}}){
		my $ch = Chanel::get_chanel($id);
		next unless $ch;

		my $last = $lgn->{chanels}{$id};
		my $messages = $ch->{messages};
		# отсылаем только последние 100 сообщений
		if(@$messages - ($last -= $ch->{shift}) > 100){
			$last = @$messages - 100;
		}
		if($last < @$messages){
			push @list,@{$messages}[$last .. $#$messages];
			$lgn->{chanels}{$id} = $ch->{shift} + @$messages;
		}
	}
	return \@list;
}
sub _i {
	1 if %Login::sids;
	1 if %Chanel::chanels;
}
1;
